package storage

import (
	"fmt"
	"io"
	"net/url"

	"github.com/mcuadros/go-defaults"
	"github.com/minio/minio-go"
	"github.com/sirupsen/logrus"
	"github.com/xuelang-group/suanpan-appconnector/pkg/util"
)

type MinioStorage struct {
	StorageMinioEndpoint   string `mapstructure:"--storage-minio-endpoint" default:"http://minio-service.default:9000"`
	StorageMinioBucketName string `mapstructure:"--storage-minio-bucket-name" default:"suanpan"`
	StorageMinioAccessKey  string `mapstructure:"--storage-minio-access-key"`
	StorageMinioSecretKey  string `mapstructure:"--storage-minio-secret-key"`
	StorageMinioTempStore  string `mapstructure:"--storage-minio-temp-store"`
}

// TODO:optimized cache or siglenton example: sync.once()
func (m *MinioStorage) getClient() (*minio.Client, error) {
	var minioStorage MinioStorage
	defaults.SetDefaults(&minioStorage)
	u, err := url.Parse(m.StorageMinioEndpoint)
	if err != nil {
		errStr := fmt.Errorf("parse StorageMinioEndpoint error: %w", err)
		return nil, &StorageError{EndpointType: Minio,
			Endpoint: m.StorageMinioEndpoint,
			Err:      errStr.Error()}
	}

	cli, err := minio.New(
		u.Host, m.StorageMinioAccessKey,
		m.StorageMinioSecretKey, u.Scheme == "https")
	if err != nil {
		return nil, &StorageError{EndpointType: Minio,
			Endpoint: m.StorageMinioEndpoint,
			Err:      err.Error()}
	}

	return cli, nil
}

func convertStorageParemterToGetObjectsOptions(param []StorageParemeter, m *minio.GetObjectOptions) error {
	if len(param) == 0 {
		return nil
	}

	if param == nil {
		return nil
	}

	return fmt.Errorf("minio get object not support yet, param%s", param)
}

func (m *MinioStorage) FGetObject(objectName, filePath string, param ...StorageParemeter) error {
	cli, err := m.getClient()
	if err != nil {
		return err
	}
	minioGetParam := minio.GetObjectOptions{}
	err = convertStorageParemterToGetObjectsOptions(param, &minioGetParam)
	if err != nil {
		logrus.Error(err)
	}

	err = cli.FGetObject(m.StorageMinioBucketName, objectName, filePath, minioGetParam)
	if err != nil {
		return &StorageError{EndpointType: Minio,
			Endpoint: m.StorageMinioEndpoint,
			Err:      err.Error()}
	} else {
		return nil
	}
}

func convertStorageParemterToPutObjectOptions(param []StorageParemeter, m *minio.PutObjectOptions) error {
	if len(param) == 0 {
		return nil
	}
	for _, p := range param {
		for k, v := range p {
			switch k {
			case "ContentType":
				m.ContentType = v
			case "CacheControl":
				m.CacheControl = v
			case "Expires":
				_ = "" //empty putObjectOptions no expires field?
			default:
				return fmt.Errorf("minio paremter:%s, not supported yet", k)
			}
		}
	}
	return nil
}

func (m *MinioStorage) FPutObject(objectName, filePath string, param ...StorageParemeter) error {
	cli, err := m.getClient()
	if err != nil {
		return err
	}

	minioPutObj := minio.PutObjectOptions{}
	err = convertStorageParemterToPutObjectOptions(param, &minioPutObj)
	if err != nil {
		logrus.Error(err)
	}

	n, err := cli.FPutObject(m.StorageMinioBucketName, objectName, filePath, minioPutObj)
	logrus.Debugf("Uploaded %d bytes, obj name:%s, path:%s", n, objectName, filePath)
	if err != nil {
		return &StorageError{EndpointType: Minio,
			Endpoint: m.StorageMinioEndpoint,
			Err:      err.Error()}
	} else {
		return nil
	}
}

func (m *MinioStorage) PutObject(objectName string, reader io.Reader, param ...StorageParemeter) error {
	cli, err := m.getClient()
	if err != nil {
		return err
	}

	minioPutObj := minio.PutObjectOptions{}
	err = convertStorageParemterToPutObjectOptions(param, &minioPutObj)
	if err != nil {
		logrus.Error(err)
	}

	n, err := cli.PutObject(m.StorageMinioBucketName, objectName, reader, -1, minioPutObj)
	logrus.Debugf("Uploaded %d bytes", n)
	if err != nil {
		return &StorageError{EndpointType: Minio,
			Endpoint: m.StorageMinioEndpoint,
			Err:      err.Error()}
	} else {
		return nil
	}
}

func (m *MinioStorage) ListObjects(objectPrefix string, recursive bool, maxKeys int, param ...StorageParemeter) ([]ObjectItem, error) {
	cli, err := m.getClient()
	if err != nil {
		return nil, err
	}

	//bug fix from wp, to avoid for loop ends and donech will never be handled
	//just use async channel to avoid block
	doneCh := make(chan struct{}, 1)
	defer close(doneCh)

	objects := make([]ObjectItem, 0)
	for o := range cli.ListObjectsV2(m.StorageMinioBucketName, objectPrefix, recursive, doneCh) {
		if o.Err != nil {
			return nil, &StorageError{EndpointType: Minio,
				Endpoint: m.StorageMinioEndpoint,
				Err:      o.Err.Error()}
		}
		objects = append(objects, ObjectItem{
			Name:         o.Key,
			LastModified: util.ISOString(o.LastModified),
		})
		if len(objects) >= maxKeys {
			//use red color to notify in logkit
			logrus.WithField("logkit", true).Errorf("[minio] list object reach maxKeys %d, break", maxKeys)
			doneCh <- struct{}{}
			break
		}
	}

	return objects, nil
}

func (m *MinioStorage) DeleteObject(objectName string, param ...StorageParemeter) error {
	cli, err := m.getClient()
	if err != nil {
		return err
	}

	err = cli.RemoveObject(m.StorageMinioBucketName, objectName)
	if err != nil {
		return &StorageError{EndpointType: Minio,
			Endpoint: m.StorageMinioEndpoint,
			Err:      err.Error()}
	} else {
		return nil
	}
}

// TODO: error handling by Storage error
func (m *MinioStorage) DeleteMultiObjects(objectNames []string, param ...StorageParemeter) error {
	cli, err := m.getClient()
	if err != nil {
		return err
	}

	objectsCh := make(chan string)
	defer close(objectsCh)
	for _, o := range objectNames {
		objectsCh <- o
	}

	go func() {
		for err := range cli.RemoveObjects(m.StorageMinioBucketName, objectsCh) {
			logrus.Errorf("Remove object error: %s", err.Err.Error())
		}
	}()

	return nil
}
